/**
 * Copyright 2021 Appsmith Inc.
 * <p>
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 * <p>
 * http://www.apache.org/licenses/LICENSE-2.0
 * <p>
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 * <p>
 */

// copied and adapted for mongo result parsing

package org.lowcoder.plugin.mongo.utils;

import static org.lowcoder.plugin.mongo.constants.MongoFieldName.COMMAND_TYPE;
import static org.lowcoder.sdk.exception.PluginCommonError.QUERY_ARGUMENT_ERROR;
import static org.lowcoder.sdk.util.JsonUtils.createObjectNode;
import static org.lowcoder.sdk.util.JsonUtils.readTree;

import java.math.BigDecimal;
import java.net.URLEncoder;
import java.nio.charset.StandardCharsets;
import java.time.Instant;
import java.time.ZoneId;
import java.time.format.DateTimeFormatter;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Comparator;
import java.util.Date;
import java.util.HashSet;
import java.util.Map;

import org.bson.Document;
import org.bson.json.JsonParseException;
import org.bson.types.Decimal128;
import org.bson.types.ObjectId;
import org.json.JSONArray;
import org.json.JSONObject;
import org.lowcoder.plugin.mongo.commands.Aggregate;
import org.lowcoder.plugin.mongo.commands.Count;
import org.lowcoder.plugin.mongo.commands.Delete;
import org.lowcoder.plugin.mongo.commands.Distinct;
import org.lowcoder.plugin.mongo.commands.Find;
import org.lowcoder.plugin.mongo.commands.Insert;
import org.lowcoder.plugin.mongo.commands.MongoCommand;
import org.lowcoder.plugin.mongo.commands.UpdateMany;
import org.lowcoder.sdk.exception.PluginException;
import org.lowcoder.sdk.models.DatasourceStructure;

import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.node.ArrayNode;
import com.fasterxml.jackson.databind.node.ObjectNode;

public class MongoQueryUtils {

    public static final String N_MODIFIED = "nModified";

    private static final String VALUE = "value";

    private static final String VALUES = "values";

    private static final DateTimeFormatter FORMATTER = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss").withZone(ZoneId.systemDefault());


    public static Document parseSafely(String fieldName, String input) {
        try {
            return Document.parse(input);
        } catch (JsonParseException e) {
            throw new PluginException(QUERY_ARGUMENT_ERROR, "INVALID_JSON_FORMAT", fieldName);
        }
    }

    public static boolean isRawCommand(Map<String, Object> formData) {
        String command = (String) formData.getOrDefault(COMMAND_TYPE, null);
        return "RAW".equalsIgnoreCase(command);
    }

    public static MongoCommand convertMongoFormInputToRawCommand(Map<String, Object> formData) {

        // Parse the commands into raw appropriately
        String commandType = (String) formData.getOrDefault(COMMAND_TYPE, "");
        MongoCommand command = switch (commandType.toUpperCase()) {
            case "INSERT" -> new Insert(formData);
            case "FIND" -> new Find(formData);
            case "UPDATE" -> new UpdateMany(formData);
            case "DELETE" -> new Delete(formData);
            case "COUNT" -> new Count(formData);
            case "DISTINCT" -> new Distinct(formData);
            case "AGGREGATE" -> new Aggregate(formData);
            default -> throw new PluginException(QUERY_ARGUMENT_ERROR, "INVALID_MONGODB_REQUEST", commandType);
        };
        if (!command.isValid()) {
            throw new PluginException(QUERY_ARGUMENT_ERROR, "INVALID_PARAM_CONFIG_PLZ_CHECK",
                    command.getFieldNamesWithNoConfiguration());
        }

        return command;
    }

    public static void generateTemplatesAndStructureForACollection(Document document,
            ArrayList<DatasourceStructure.Column> columns) {
        String filterFieldName = null;
        for (Map.Entry<String, Object> entry : document.entrySet()) {
            final String name = entry.getKey();
            final Object value = entry.getValue();
            String type;
            boolean isAutogenerated = false;

            if (value instanceof Integer) {
                type = "Integer";
            } else if (value instanceof Long) {
                type = "Long";
            } else if (value instanceof Double) {
                type = "Double";
            } else if (value instanceof Decimal128) {
                type = "BigDecimal";
            } else if (value instanceof String) {
                type = "String";
                if (filterFieldName == null || filterFieldName.compareTo(name) > 0) {
                    filterFieldName = name;
                }
            } else if (value instanceof ObjectId) {
                type = "ObjectId";
                isAutogenerated = true;
            } else if (value instanceof Collection) {
                type = "Array";
            } else if (value instanceof Date) {
                type = "Date";
            } else {
                type = "Object";
            }

            columns.add(new DatasourceStructure.Column(name, type, null, isAutogenerated));
        }

        columns.sort(Comparator.naturalOrder());
    }

    public static String urlEncode(String text) {
        return URLEncoder.encode(text, StandardCharsets.UTF_8);
    }

    public static JsonNode parseResultBody(JSONObject outputJson) throws JsonProcessingException {
            /*
             For the `findAndModify` command, we don't get the count of modifications made. Instead,
             we either get the modified new value or the pre-modified old value (depending on the
             `new` field in the command. Let's return that value to the user.
             */
        if (outputJson.has(VALUE)) {
            return readTree(cleanUp(new JSONObject().put(VALUE, outputJson.get(VALUE))).toString());
        }

            /*
             The json contains key "cursor" when find command was issued and there are 1 or more
             results. In case there are no results for find, this key is not present in the result json.
             */
        if (outputJson.has("cursor")) {
            JSONArray outputResult = (JSONArray) cleanUp(
                    outputJson.getJSONObject("cursor").getJSONArray("firstBatch"));
            return readTree(outputResult.toString());
        }

            /*
             The json contains key "n" when insert/update command is issued. "n" for update
             signifies the no of documents selected for update. "n" in case of insert signifies the
             number of documents inserted.
             */
        if (outputJson.has("n")) {
            JSONObject body = new JSONObject().put("n", outputJson.getBigInteger("n"));
            return readTree(body.toString());
        }

            /*
             The json key contains key "nModified" in case of update command. This signifies the no of
             documents updated.
             */
        if (outputJson.has(N_MODIFIED)) {
            JSONObject body = new JSONObject().put(N_MODIFIED, outputJson.getBigInteger(N_MODIFIED));
            return readTree(body.toString());
        }

            /*
             The json contains key "values" when distinct command is used.
             */
        if (outputJson.has(VALUES)) {
            JSONArray outputResult = (JSONArray) cleanUp(
                    outputJson.getJSONArray(VALUES));

            ObjectNode resultNode = createObjectNode();

            // Create a JSON structure with the results stored with a key to abide by the
            // Server-Client contract of only sending array of objects in result.
            resultNode.putArray(VALUES)
                    .addAll((ArrayNode) readTree(outputResult.toString()));

            return readTree(resultNode.toString());
        }

        return null;
    }

    private static Object cleanUp(Object object) {
        if (object instanceof JSONObject jsonObject) {
            final boolean isSingleKey = jsonObject.keySet().size() == 1;

            if (isSingleKey && "$numberLong".equals(jsonObject.keys().next())) {
                return jsonObject.getBigInteger("$numberLong");

            } else if (isSingleKey && "$oid".equals(jsonObject.keys().next())) {
                return jsonObject.getString("$oid");

            } else if (isSingleKey && "$date".equals(jsonObject.keys().next())) {
                Instant instant;
                if (jsonObject.get("$date") instanceof Long millis) {
                    instant = Instant.ofEpochMilli(millis);
                } else {
                    instant = Instant.parse(jsonObject.getString("$date"));
                }
                return FORMATTER.format(instant);
            } else if (isSingleKey && "$numberDecimal".equals(jsonObject.keys().next())) {
                return new BigDecimal(jsonObject.getString("$numberDecimal"));

            } else {
                for (String key : new HashSet<>(jsonObject.keySet())) {
                    jsonObject.put(key, cleanUp(jsonObject.get(key)));
                }

            }

        } else if (object instanceof JSONArray) {
            Collection<Object> cleaned = new ArrayList<>();

            for (Object child : (JSONArray) object) {
                cleaned.add(cleanUp(child));
            }

            return new JSONArray(cleaned);

        }

        return object;
    }

}
